﻿//#define RECORDE_TIME_JOB
#if UNITY_EDITOR || COMMAND_PROFILER
#define PROFILING
using UnityEngine.Profiling;
#endif
using System.Collections.Generic;
using UnityEngine;
using System.Collections;
#if RECORDE_TIME_JOB
using Unity.Burst;
using Unity.Jobs;
using Unity.Collections;
#endif


namespace GameToolkit
{
    [DefaultExecutionOrder(1000)]
    [ExecuteInEditMode, RequireComponent(typeof(Camera)), DisallowMultipleComponent, AddComponentMenu("Game Toolkit/Culling Camera")]
    public partial class CullingCamera : MonoBehaviour
    {
#if PROFILING
        readonly string profiling_updateBounds = "Update Dynamic CullingBounding";
        //readonly string profiling_preAction = "Pre Culling Actions";
        readonly string profiling_beforeCulling = "Before Culling Action";
        readonly string profiling_update = "Update Culling Targets";
        readonly string profiling_lateUpdate = "LateUpdate Culling Targets";
        readonly string profiling_fixedUpdate = "FixedUpdate Culling Targets";
        readonly string profiling_backUpdate = "Background Culling Targets";
        readonly string profiling_recordDeltaTime = "Recorde DeltaTime";
#endif
        // 记录 deltaTime 的最大帧数，以此来获取近期若干帧之间的累积 deltaTime, 必须定义为 2的指数
#if RECORDE_TIME_JOB
        readonly int record_deltaTime_frames = 512;

        // 计算累积帧 deltaTime
#if BURST_COMPILE
        [BurstCompile]
#endif
        struct CalculateDeltaTime : IJob
        {
            public int size;
            public int recentFrameIndex;
            [ReadOnly, NativeDisableParallelForRestriction]
            public NativeArray<float> recentDeltaTime;
            [NativeDisableParallelForRestriction]
            public NativeArray<float> recentDeltaTimeResult;

            public void Execute()
            {
                float t = 0;
                int mask = size - 1;
                for (int i = 0; i < size; i++)
                {
                    t += recentDeltaTime[(recentFrameIndex + i) & mask];
                    recentDeltaTimeResult[i] = t;
                }
            }
        }
#else
        readonly int record_deltaTime_frames = 128;
#endif

        internal class UpdateNode
        {
            internal byte stat_distance;
            internal bool stat_visible;
            internal int stat_frameId;

            internal byte cullDistance;
            internal byte alwaysDistance;
            internal byte updateGrp;
            internal byte updatingGrp; // 当前所在更新组
            internal ICullingUpdate update;
            internal UpdateNode previours;
            internal UpdateNode next;
#if PROFILING
            public string profiling;
            public Object profilingContext;
#endif

            static UpdateNode cachedNode; // a <- b <- c <- cachedNode

            public static UpdateNode GetNode(ICullingUpdate target, byte grp)
            {
                UpdateNode node;
                if (cachedNode == null)
                {
                    node = new UpdateNode();
                }
                else
                {
                    node = cachedNode;
                    cachedNode = cachedNode.previours;
                    node.previours = null;
                }
                node.update = target;
                node.updateGrp = grp;
                node.updatingGrp = 0;
                node.cullDistance = GetDistanceLv(target.CulledUpdateDistanceLv);
                node.alwaysDistance = GetDistanceLv(target.AlwaysUpdateDistanceLv);
                node.stat_frameId = Time.frameCount;
#if PROFILING
                node.profiling = target == null ? "Null" : target.ToString();
                node.profilingContext = target as Object;
#endif
                return node;
            }

            public static void ReleaseNode(UpdateNode node)
            {
                node.previours = cachedNode;
                node.next = null;
                cachedNode = node;
            }
            static byte GetDistanceLv(int lv)
            {
                return (byte)(lv < 0 ? 0 : (lv > 5 ? 5 : lv));
            }

        }

        internal class UpdateGroup
        {
            UpdateNode root;
            // 迭代节点
            UpdateNode iterator;
            //#if UNITY_EDITOR
            //            HashSet<UpdateNode> sExecutions = new HashSet<UpdateNode>();
            //#endif

            public int Update(CullingCamera cam, ECullingUpdateMode mode, int maxCount)
            {
                if (iterator == null)
                {
                    iterator = root;
                }
                int n = 0;
                //#if UNITY_EDITOR
                //                sExecutions.Clear();
                //#endif
                while (iterator != null && (n < maxCount || maxCount <= 0))
                {
                    var it = iterator;
                    iterator = iterator.next;
                    n++;
#if PROFILING
                    Profiler.BeginSample(it.profiling, it.profilingContext);
                    //Debug.Assert(sExecutions.Add(iterator));
                    try
                    {
#endif
                        cam.mCurrentDeltaTime = mode == ECullingUpdateMode.FixedUpdate ? Time.fixedDeltaTime : cam.GetDeltaTime(it.stat_frameId);
                        cam.mCurrentVisible = it.stat_visible;
                        cam.mCurrentDistanceLv = it.stat_distance;

                        it.stat_frameId = cam.mFrameId;
                        it.update.OnCullingUpdate(cam, mode);
#if PROFILING
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogException(e, it.profilingContext);
                    }
                    Profiler.EndSample();
#endif
                }
                return n;
            }

            public void AddNode(UpdateNode node)
            {
                if (root == null)
                {
                    root = node;
                    node.previours = null;
                    node.next = null;
                }
                else
                {
                    var next = root.next;
                    root.next = node;
                    node.previours = root;
                    node.next = next;
                    if (next != null)
                    {
                        next.previours = node;
                    }
                }
            }

            public void RemoveNode(UpdateNode node)
            {
                if (node == iterator)
                {
                    iterator = node.next;
                }
                if (node == root)
                {
                    root = node.next;
                    if (root != null)
                        root.previours = null;
                }
                var p = node.previours;
                var n = node.next;
                node.next = null;
                node.previours = null;
                if (p != null)
                {
                    p.next = n;
                }
                if (n != null)
                {
                    n.previours = p;
                }
            }

            public void Clear()
            {
                this.root = null;
                this.iterator = null;
            }
        }

        internal class CullingData
        {
            public ICullingBounds target;
            public bool visible;
            public int distance;
            public UpdateNode[] updateNodes;
            public CullingData()
            {
                target = null;
                visible = false;
                distance = 10000;
            }

            internal void SetupUpdateNode(RandomList<UpdateNode> lst)
            {
                var len = updateNodes == null ? 0 : updateNodes.Length;
                if (len != lst.Count)
                {
                    updateNodes = new UpdateNode[lst.Count];
                }
                for (int i = 0; i < lst.Count; i++)
                {
                    updateNodes[i] = lst[i];
                }
            }

            internal void ValidateState(CullingCamera cam, bool visible, int distanceLv)
            {
                this.visible = visible;
                this.distance = distanceLv;
                if (target != null)
                {
                    target.ValidateCullingState(cam, visible, distanceLv);
                    cam.ValidateUpdateState(updateNodes, visible, distanceLv);
                }
            }

            internal void Release(CullingCamera cam)
            {
                if (target != null)
                {
                    var cull = target;
                    target = null;
                    cull.ReleaseCullingCamera(cam);
                    cam.ReleaseUpdateState(updateNodes);
                }
            }

            internal CullingState GetState(CullingCamera cam)
            {
                CullingState stat;
                stat.camera = cam;
                stat.bounds = target;
                stat.visible = visible;
                stat.distanceLv = distance;
                stat.updating = false;
                stat.background = false;
                if (updateNodes != null)
                {
                    for (int i = 0; i < updateNodes.Length; i++)
                    {
                        var nd = updateNodes[i];
                        stat.updating |= (nd.updatingGrp & cam.grp_frontUpdate) != 0;
                        stat.background |= (nd.updatingGrp & cam.grp_background) != 0;
                        if (stat.updating && stat.background)
                            break;
                    }
                }
#if UNITY_EDITOR
                stat.type = 0;
#endif
                return stat;
            }
        }

        // LocalCullingData will always keep visible & distanceLv: 0
        class LocalCullingData
        {
            CullingCamera mCam;
            ICullingTag[] mTags;
            Rigidbody[] mRigs;
            ICullingAction[] mActions;
            ICullingComponent[] mComponents;
            UpdateNode[] updateNodes;
            bool mActive;

            public LocalCullingData(CullingCamera cam)
            {
                mCam = cam;
                CullingBounds.FindCullingTags(cam.gameObject, (1 << cam.gameObject.layer) | cam.mCullingMask, out mTags, out mActions, out mComponents, out mRigs);
            }

            internal void Awake()
            {
                if (mActive)
                    return;
                mActive = true;
                mCam.mTempList.Clear();
                var len = mTags == null ? 0 : mTags.Length;
                for (int n = 0; n < len; n++)
                {
                    var upd = mTags[n] as ICullingUpdate;
                    if (upd == null || ((byte)upd.UpdateMode & mCam.grp_all) == 0)
                        continue;
                    mCam.GetUpdateNodes(upd, mCam.mTempList);
                }
                if (updateNodes == null || updateNodes.Length != mCam.mTempList.Count)
                    updateNodes = new UpdateNode[mCam.mTempList.Count];
                for (int i = 0; i < updateNodes.Length; i++)
                {
                    updateNodes[i] = mCam.mTempList[i];
                }
                mCam.mTempList.Cleanup();
                mCam.ValidateUpdateState(updateNodes, true, 0);
                len = mComponents == null ? 0 : mComponents.Length;
                for (int i = 0; i < len; i++)
                {
                    mComponents[i].OnCullingEnabled(mCam, null);
                }
                len = mActions == null ? 0 : mActions.Length;
                for (int i = 0; i < len; i++)
                {
                    mActions[i].OnCulling(mCam, true, 0);
                }
            }

            internal void Release()
            {
                if (!mActive)
                    return;
                mActive = false;
                mCam.ReleaseUpdateState(updateNodes);
                var len = mActions == null ? 0 : mActions.Length;
                for (int i = 0; i < len; i++)
                {
                    mActions[i].OnCulling(mCam, false, -1);
                }
                len = mComponents == null ? 0 : mComponents.Length;
                for (int i = 0; i < len; i++)
                {
                    mComponents[i].OnCullingDisabled();
                }
            }
        }


#pragma warning disable CS0649

        internal const byte ID_UPDATE = 0x1;
        internal const byte ID_FIXED_UPDATE = 0x2;
        internal const byte ID_FAR_BACKGROUD = 0x4;
        internal const byte ID_LATE_UPDATE = 0x8;
        internal const byte ID_NEAR_BACKGROUND = 0x10;

        readonly byte grp_none = 0;
        readonly byte grp_all = ID_UPDATE | ID_FIXED_UPDATE | ID_NEAR_BACKGROUND | ID_LATE_UPDATE | ID_FAR_BACKGROUD;
        readonly byte grp_frontUpdate = ID_UPDATE | ID_FIXED_UPDATE | ID_LATE_UPDATE;
        readonly byte grp_background = ID_FAR_BACKGROUD | ID_NEAR_BACKGROUND;
        //readonly byte AllUpdateGroup = (byte)(ECullingUpdateMode.Background | ECullingUpdateMode.FixedUpdate | ECullingUpdateMode.Update | ECullingUpdateMode.LateUpdate);
        //readonly byte FrontUpdateGroup = (byte)(ECullingUpdateMode.Update | ECullingUpdateMode.FixedUpdate | ECullingUpdateMode.LateUpdate);
        //readonly byte NearBackUpdateGroup = (byte)ECullingUpdateMode.NearBackground;
        //readonly byte FarBackUpdateGroup = (byte)ECullingUpdateMode.FarBackground;

        [SerializeField]
        private LayerMask m_ExcludeLayers = 0;
        int mCullingMask;
        public int CullingLayers { get { return mCullingMask; } }

        [SerializeField]
        private int m_CullingCapacity = 512;

        [SerializeField]
        Transform m_DistanceReference;

        [SerializeField]
        private Vector4 m_DistanceLv = new Vector4(30, 60, 150, 300);

        [SerializeField, Range(1, 128)]
        private int m_BatchUpdateCount = 64;

        // culling api
        private CullingGroup mCullingGroup;
        private BoundingSphere[] mCullingSpheres;
        // sort by: static, dynamic
        private CullingData[] mCullingTarget;
        LocalCullingData mLocalCullingData;
        // 当前帧索引
        int mRencentFrameIndex;
        int mFrameId;
#if RECORDE_TIME_JOB
        // 近期单帧帧 deltaTime
        NativeArray<float> mRecentDeltaTime;
        // 近期帧累积 deltaTime
        NativeArray<float> mRecentDeltaTimeResult;
#else
        float[] mRecentDeltaTime;
        float[] mRecentDeltaTimeResult;
#endif

        private int mCullLength;
        private bool mUpdateCullingGroup;
        private readonly float[] mDistanceLvs = new float[4];
        public int CullLength { get { return mCullLength; } }

        // 绑定摄像机
        Camera mCamera;
        // 当前裁剪摄像机
        Camera mCullingCamera;
        RandomList<IPreCullAction> mPreCulls;
        RandomList<IBeforeCullingUpdate> mBeforeUpdate;
        RandomList<IAfterCullingUpdate> mAfterUpdate;
        float mCurrentDeltaTime;
        bool mCurrentVisible;
        int mCurrentDistanceLv;
        /// <summary>
        /// 当前刷新组的时间间隔 (CullingUpdate 中可获取)
        /// </summary>
        public float deltaTime { get { return mCurrentDeltaTime; } }
        /// <summary>
        /// 当前对象是否可见 (CullingUpdate 中可获取)
        /// </summary>
        public bool visible { get { return mCurrentVisible; } }
        /// <summary>
        /// 当前对象距离等级 (CullingUpdate 中可获取)
        /// </summary>
        public int distanceLv { get { return mCurrentDistanceLv; } }

        // Update 节点
        UpdateGroup mUpdateRoot;
        UpdateGroup mLateUpdateRoot;
        // FixedUpdate 节点
        UpdateGroup mFixedUpdateRoot;
        // Background 节点
        UpdateGroup mBackgroundRoot;
        UpdateGroup mFarBackgroundRoot;
        int mUpdateNum;
        RandomList<UpdateNode> mTempList = new RandomList<UpdateNode>(32, (x, y) => x == y, true);
        bool mDisposing;
        bool mStarted;

        public Transform DistanceReference
        {
            get { return m_DistanceReference; }
            set
            {
                if (m_DistanceReference != value)
                {
                    m_DistanceReference = value;
                    if (mCullingGroup != null && mCullingGroup.enabled)
                    {
                        if (value != null)
                            mCullingGroup.SetDistanceReferencePoint(value);
                        else if (mCamera != null)
                            mCullingGroup.SetDistanceReferencePoint(mCamera.transform);
                    }
                }
            }
        }

        public Camera ActiveCamera { get { return mCullingCamera; } }
        public Camera AttachedCamera { get { return mCamera; } }

        float GetDeltaTime(int frameId)
        {
            var frames = mFrameId - frameId - 1;
            if (frames < 0)
                return mRecentDeltaTime[0];
            else if (frames >= record_deltaTime_frames)
                return mRecentDeltaTimeResult[record_deltaTime_frames - 1];
            else
                return mRecentDeltaTimeResult[frames];
        }

#pragma warning restore

        UpdateGroup GetUpdateGroup(byte grp)
        {
            if (grp == ID_UPDATE)
                return mUpdateRoot;
            else if (grp == ID_LATE_UPDATE)
                return mLateUpdateRoot;
            else if (grp == ID_FIXED_UPDATE)
                return mFixedUpdateRoot;
            else if (grp == ID_NEAR_BACKGROUND)
                return mBackgroundRoot;
            else if (grp == ID_FAR_BACKGROUD)
                return mFarBackgroundRoot;
            else
                return null;
        }

        public int GetDistanceLv(float distance)
        {
            if (distance < m_DistanceLv.x)
                return 0;
            if (distance < m_DistanceLv.y)
                return 1;
            if (distance < m_DistanceLv.z)
                return 2;
            if (distance < m_DistanceLv.w)
                return 3;
            return 4;
        }

#if !UNITY_EDITOR
        private void Awake()
        {
            mLocalCullingData = new LocalCullingData(this);
        }
#endif

        private void InitCullingDatas()
        {
            //#if UNITY_EDITOR
            //            if (GlobalUtil.IsPrefabStage(gameObject))
            //                return;
            //#endif
#if RECORDE_TIME_JOB
            mRecentDeltaTime = new NativeArray<float>(record_deltaTime_frames, Allocator.Persistent);
            mRecentDeltaTimeResult = new NativeArray<float>(record_deltaTime_frames, Allocator.Persistent);
#else
            mRecentDeltaTime = new float[record_deltaTime_frames];
            mRecentDeltaTimeResult = new float[record_deltaTime_frames];
#endif
            mLocalCullingData = new LocalCullingData(this);
            mCamera = GetComponent<Camera>();
            mUpdateRoot = new UpdateGroup();
            mLateUpdateRoot = new UpdateGroup();
            mFixedUpdateRoot = new UpdateGroup();
            mBackgroundRoot = new UpdateGroup();
            mFarBackgroundRoot = new UpdateGroup();
            mCullingMask = (mCamera == null ? 0 : mCamera.cullingMask) & ~m_ExcludeLayers;
            SetCullingCamera(mCamera);
            if (mPreCulls == null)
                mPreCulls = new RandomList<IPreCullAction>(8);
            if (mBeforeUpdate == null)
                mBeforeUpdate = new RandomList<IBeforeCullingUpdate>(8);
            if (mAfterUpdate == null)
                mAfterUpdate = new RandomList<IAfterCullingUpdate>(8);
            CullingManager.Instance.GetCullingTargets(this);
            if (mCullingGroup != null)
                mCullingGroup.enabled = true;
            mLocalCullingData.Awake();

#if UNITY_EDITOR
            DebugCamera.ForceReset = true;
#endif
        }

        private void OnEnable()
        {
            if (mStarted)
            {
                InitCullingDatas();
            }
        }

        private void Start()
        {
            mStarted = true;
            InitCullingDatas();
        }

        private void OnDisable()
        {
            mDisposing = true;
#if RECORDE_TIME_JOB
            mRecentDeltaTime.Dispose();
            mRecentDeltaTime = default;
            mRecentDeltaTimeResult.Dispose();
            mRecentDeltaTimeResult = default;
#endif
            if (mLocalCullingData != null)
            {
                mLocalCullingData.Release();
            }
            CullingManager.Instance.ReleaseCullingTargets(this);
            if (mCullingGroup != null)
            {
                mCullingGroup.Dispose();
                mCullingGroup = null;
            }
            mCullLength = 0;
            mCullingSpheres = null;
            mCullingTarget = null;
            SetCullingCamera(null);
            mDisposing = false;
        }

        private void OnDestroy()
        {
            mLocalCullingData = null;
            if (mCullingGroup != null)
            {
                mCullingGroup.Dispose();
                mCullingGroup = null;
                CullingManager.Instance.ReleaseCullingTargets(this);
                mCullLength = 0;
                mCullingSpheres = null;
                mCullingTarget = null;
                SetCullingCamera(null);
            }
        }

        internal void OnPreCull()
        {
            //#if PROFILING
            //            Profiler.BeginSample(profiling_preAction, this);
            //#endif
            if (mCamera != null)
            {
                for (int i = 0; i < mPreCulls.Count; i++)
                {
                    mPreCulls[i].OnPreCullAction(mCamera);
                }
            }
            //#if PROFILING
            //            Profiler.EndSample();
            //#endif
        }

        void ValidateUpdateState(UpdateNode[] updateNodes, bool visible, int distanceLv)
        {
            var len = updateNodes == null ? 0 : updateNodes.Length;
            for (int i = 0; i < len; i++)
            {
                byte grp;
                var node = updateNodes[i];
                node.stat_distance = (byte)distanceLv;
                node.stat_visible = visible;
                if (distanceLv >= 0)
                {
                    var update = (node.alwaysDistance > distanceLv || (visible && node.cullDistance > distanceLv));
                    if (update)
                        grp = (byte)(grp_frontUpdate & node.updateGrp);
                    else if (visible || node.cullDistance < 1)
                        grp = (byte)(ID_NEAR_BACKGROUND & node.updateGrp);
                    else
                        grp = (byte)(ID_FAR_BACKGROUD & node.updateGrp);
                }
                else
                {
                    grp = 0;
                }
                if (grp != node.updatingGrp)
                {
                    var group = GetUpdateGroup(node.updatingGrp);
                    var group2 = GetUpdateGroup(grp);
                    node.updatingGrp = grp;
                    if (group != null)
                        group.RemoveNode(node);
                    if (group2 != null)
                        group2.AddNode(node);
                }
            }
        }

        void ReleaseUpdateState(UpdateNode[] updateNodes)
        {
            var len = updateNodes == null ? 0 : updateNodes.Length;
            for (int i = 0; i < len; i++)
            {
                var node = updateNodes[i];
                var grp = GetUpdateGroup(node.updatingGrp);
                if (grp != null)
                    grp.RemoveNode(node);
                UpdateNode.ReleaseNode(node);
                updateNodes[i] = null;
            }
        }

        private void Update()
        {
            if (mCullingGroup != null)
            {
#if PROFILING
                Profiler.BeginSample(profiling_recordDeltaTime);
#endif
                mFrameId = Time.frameCount;
                mRecentDeltaTime[mRencentFrameIndex] = Time.deltaTime;

                int mask = record_deltaTime_frames - 1;
#if RECORDE_TIME_JOB
                CalculateDeltaTime job;
                job.size = record_deltaTime_frames;
                job.recentFrameIndex = mRencentFrameIndex;
                job.recentDeltaTime = mRecentDeltaTime;
                job.recentDeltaTimeResult = mRecentDeltaTimeResult;
                var handler = job.Schedule();
                handler.Complete();
#else
                float t = 0;
                for (int i = 0; i < record_deltaTime_frames; i++)
                {
                    t += mRecentDeltaTime[(i + mRencentFrameIndex) & mask];
                    mRecentDeltaTimeResult[i] = t;
                }
#endif
                mRencentFrameIndex = (mRencentFrameIndex + 1) & mask;
#if PROFILING
                Profiler.EndSample();
                Profiler.BeginSample(profiling_beforeCulling, this);
#endif
                var len = mBeforeUpdate == null ? 0 : mBeforeUpdate.Count;
                for (int i = len - 1; i >= 0; i--)
                {
                    mBeforeUpdate[i].OnBeforeCullingUpdate(mCamera);
                }
#if PROFILING
                Profiler.EndSample();
                Profiler.BeginSample(profiling_updateBounds, this);
#endif
                if (mUpdateCullingGroup)
                {
                    mUpdateCullingGroup = false;
                    while (mCullLength > 0 && mCullingTarget[mCullLength - 1].target == null)
                    {
                        mCullingTarget[--mCullLength] = null;
                    }
                    mCullingGroup.SetBoundingSphereCount(mCullLength);
                }
#if PROFILING
                Profiler.EndSample();
                Profiler.BeginSample(profiling_update, this);
#endif
                mUpdateNum = mUpdateRoot == null ? 0 : mUpdateRoot.Update(this, ECullingUpdateMode.Update, -1);
#if PROFILING
                Profiler.EndSample();
#endif
            }
        }

        private void LateUpdate()
        {
            if (mCullingGroup != null)
            {
#if PROFILING
                Profiler.BeginSample(profiling_lateUpdate, this);
#endif
                mUpdateNum += mLateUpdateRoot.Update(this, ECullingUpdateMode.LateUpdate, -1);

#if PROFILING
                Profiler.EndSample();
                Profiler.BeginSample(profiling_backUpdate, this);
#endif
                var bglen = Mathf.Max(1, m_BatchUpdateCount - mUpdateNum);
                mUpdateNum += mBackgroundRoot.Update(this, ECullingUpdateMode.Background, bglen);
                bglen = Mathf.Max(1, m_BatchUpdateCount - mUpdateNum);
                mFarBackgroundRoot.Update(this, ECullingUpdateMode.Background, bglen);
#if PROFILING
                Profiler.EndSample();
#endif
                var len = mAfterUpdate == null ? 0 : mAfterUpdate.Count;
                for (int i = len - 1; i >= 0; i--)
                {
                    mAfterUpdate[i].OnAfterCullingUpdate(mCamera);
                }
            }
        }

        private void FixedUpdate()
        {
            if (mCullingGroup != null)
            {
#if PROFILING
                Profiler.BeginSample(profiling_fixedUpdate, this);
#endif
                if (mFixedUpdateRoot != null)
                {
                    mFixedUpdateRoot.Update(this, ECullingUpdateMode.FixedUpdate, -1);
                }
#if PROFILING
                Profiler.EndSample();
#endif
            }
        }

        public void Recalculate(int index)
        {
            mCullingSpheres[index] = mCullingTarget[index].target.Bounding;
        }

        private void InitCapacity(int cap)
        {
            var len = mCullingSpheres == null ? 0 : mCullingSpheres.Length;
            cap = Mathf.Max(256, cap, mCullLength, m_CullingCapacity);
            if (len != cap)
            {
                var sphs = mCullingSpheres;
                var tars = mCullingTarget;
                mCullingSpheres = new BoundingSphere[cap];
                mCullingTarget = new CullingData[cap];
                if (mCullLength > 0)
                {
                    System.Array.Copy(sphs, mCullingSpheres, mCullLength);
                    System.Array.Copy(tars, mCullingTarget, mCullLength);
                }
                if (mCullingGroup != null)
                    mCullingGroup.SetBoundingSpheres(mCullingSpheres);
            }
        }

        public void SetCullingCamera(Camera cam)
        {
            mCullingCamera = cam;
            if (cam != null)
            {
                if (mCullingSpheres == null)
                    InitCapacity(m_CullingCapacity);
                if (mCullingGroup == null)
                {
                    mDistanceLvs[0] = m_DistanceLv.x;
                    mDistanceLvs[1] = m_DistanceLv.y;
                    mDistanceLvs[2] = m_DistanceLv.z;
                    mDistanceLvs[3] = m_DistanceLv.w;
                    mCullingGroup = new CullingGroup();
                    mCullingGroup.SetBoundingDistances(mDistanceLvs);
                    mCullingGroup.SetBoundingSpheres(mCullingSpheres);
                    mCullingGroup.SetBoundingSphereCount(mCullLength);
                    mCullingGroup.onStateChanged = OnCullStateChanged;
                }
                if (m_DistanceReference != null)
                    mCullingGroup.SetDistanceReferencePoint(m_DistanceReference);
                else
                    mCullingGroup.SetDistanceReferencePoint(cam.transform);
                mCullingGroup.targetCamera = cam;
                mCullingGroup.enabled = true;
            }
            else if (mCullingGroup != null)
            {
                mCullingGroup.enabled = false;
            }
        }

        private void OnCullStateChanged(CullingGroupEvent sphere)
        {
            var t = mCullingTarget[sphere.index];
            if (t == null)
                return;
            t.ValidateState(this, sphere.isVisible, sphere.currentDistance);
        }

        public void ClearCullingTarget()
        {
            for (int i = 0; i < mCullLength; i++)
            {
                var cull = mCullingTarget[i];
                cull.Release(this);
                mCullingTarget[i] = null;
            }
            mCullLength = 0;
            if (mUpdateRoot != null)
            {
                mUpdateRoot.Clear();
            }
            if (mFixedUpdateRoot != null)
            {
                mFixedUpdateRoot.Clear();
            }
            if (mBackgroundRoot != null)
            {
                mBackgroundRoot.Clear();
            }
            if (mFarBackgroundRoot != null)
            {
                mFarBackgroundRoot.Clear();
            }
            if (mCullingGroup != null)
            {
                mCullingGroup.SetBoundingSphereCount(0);
            }
            if (mPreCulls != null)
                mPreCulls.Clear();
        }

        void GetUpdateNodes(ICullingUpdate upd, ICollection<UpdateNode> lst)
        {
            byte bgp = (upd.UpdateMode & ECullingUpdateMode.Background) == 0 ? grp_none : grp_background;
            byte grp;
            grp = (byte)(upd.UpdateMode & ECullingUpdateMode.LateUpdate);
            if (grp != 0)
            {
                var node = UpdateNode.GetNode(upd, (byte)(grp | bgp));
                bgp = grp_none;
                lst.Add(node);
            }
            grp = (byte)(upd.UpdateMode & ECullingUpdateMode.Update);
            if (grp != 0)
            {
                var node = UpdateNode.GetNode(upd, (byte)(grp | bgp));
                bgp = grp_none;
                lst.Add(node);
            }
            grp = (byte)(upd.UpdateMode & ECullingUpdateMode.FixedUpdate);
            if (grp != 0)
            {
                var node = UpdateNode.GetNode(upd, (byte)(grp | bgp));
                bgp = grp_none;
                lst.Add(node);
            }
            if (bgp != grp_none)
            {
                var node = UpdateNode.GetNode(upd, bgp);
                lst.Add(node);
            }
        }

        bool SetupTarget(CullingData cull, int index, ICullingBounds target)
        {
            var setup = target.HandleCullingCamera(new CullingHandle(this, index, target));
            if (setup)
            {
                cull.target = target;
                mTempList.Clear();
                var len = target.TagCount;
                for (int n = 0; n < len; n++)
                {
                    var upd = target.GetCullingTag(n) as ICullingUpdate;
                    if (upd == null || ((byte)upd.UpdateMode & grp_all) == 0)
                        continue;
                    GetUpdateNodes(upd, mTempList);
                }
                cull.SetupUpdateNode(mTempList);
                mTempList.Cleanup();
                mCullingSpheres[index] = target.Bounding;
                cull.ValidateState(this, cull.visible, cull.distance);
            }
            return setup;
        }

        internal void AddCullingTarget(ICullingBounds target)
        {
            if (mDisposing)
                return;
            for (int i = 0; i < mCullLength; i++)
            {
                var cull = mCullingTarget[i];
                if (cull.target == null)
                {
                    SetupTarget(cull, i, target);
                    return;
                }
            }
            AppendCullingTarget(target);
        }

        internal void AppendCullingTarget(ICullingBounds target)
        {
            if (mDisposing)
                return;
            if (mCullingSpheres == null || mCullingSpheres.Length == mCullLength)
                InitCapacity(mCullLength << 1);
            var cull = mCullingTarget[mCullLength];
            if (cull == null)
            {
                cull = new CullingData();
                cull.visible = false;
                cull.distance = -1;
                mCullingTarget[mCullLength] = cull;
            }
            if (SetupTarget(cull, mCullLength, target))
            {
                mCullLength++;
                mUpdateCullingGroup = true;
            }
        }

        internal void RemoveCullingTarget(int cullingIndex, ICullingBounds target)
        {
            if (mDisposing)
                return;
            if (cullingIndex < mCullLength && cullingIndex >= 0 && mCullingTarget[cullingIndex].target == target)
            {
                var cull = mCullingTarget[cullingIndex];
                cull.Release(this);
                mCullingSpheres[cullingIndex] = new BoundingSphere(new Vector3(0, -10000, 0), 0);
                mUpdateCullingGroup |= cullingIndex == mCullLength - 1;
                return;
            }
        }

        internal void AddCullingEvent(ICullEvent action)
        {
            if (action is IPreCullAction && !mPreCulls.Contains((IPreCullAction)action))
                mPreCulls.Add((IPreCullAction)action);
            if (action is IBeforeCullingUpdate && !mBeforeUpdate.Contains((IBeforeCullingUpdate)action))
                mBeforeUpdate.Add((IBeforeCullingUpdate)action);
            if (action is IAfterCullingUpdate && !mAfterUpdate.Contains((IAfterCullingUpdate)action))
                mAfterUpdate.Add((IAfterCullingUpdate)action);
        }

        internal void AppendCullingEvent(ICullEvent action)
        {
            if (action is IPreCullAction)
                mPreCulls.Add((IPreCullAction)action);
            if (action is IBeforeCullingUpdate)
                mBeforeUpdate.Add((IBeforeCullingUpdate)action);
            if (action is IAfterCullingUpdate)
                mAfterUpdate.Add((IAfterCullingUpdate)action);
        }

        internal void RemovePreCullAction(ICullEvent action)
        {
            if (action is IPreCullAction)
                mPreCulls.Remove((IPreCullAction)action);
            if (action is IBeforeCullingUpdate)
                mBeforeUpdate.Remove((IBeforeCullingUpdate)action);
            if (action is IAfterCullingUpdate)
                mAfterUpdate.Remove((IAfterCullingUpdate)action);
        }

        public IEnumerator<CullingState> GetEnumerator(int distanceLv)
        {
            return new Enumerator(this, distanceLv);
        }

        private struct Enumerator : IEnumerator<CullingState>
        {
            int distanceLv;
            CullingCamera cullCam;
            int index;
            CullingState current;

            public Enumerator(CullingCamera cam, int distanceLv)
            {
                this.distanceLv = distanceLv;
                this.cullCam = cam;
                this.index = -1;
                current = default;
            }

            public CullingState Current { get { return current; } }

            object IEnumerator.Current { get { return current; } }

            public void Dispose()
            {
                cullCam = null;
            }

            public bool MoveNext()
            {
                while (index < cullCam.mCullLength)
                {
                    index++;
                    if (index < cullCam.mCullLength && cullCam.mCullingTarget[index].target != null
                        && cullCam.mCullingTarget[index].distance < distanceLv)
                    {
                        var cull = cullCam.mCullingTarget[index];
                        current = cull.GetState(cullCam);
                        return true;
                    }
                }
                return false;
            }

            public void Reset()
            {
                this.index = -1;
            }
        }

    }

}